<?php

namespace App\Http\Controllers;

use Carbon\Carbon;
use App\AssetIndex;
use App\Repository;
use App\RequestLog;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;

use InfluxDB2\Client;
use InfluxDB2\Model\WritePrecision;

class RepositoryController extends Controller
{
    public function __construct()
    {
        $this->output    =  new \Symfony\Component\Console\Output\ConsoleOutput();
        $this->disk      =  Storage::disk('public');
        if(env('INFLUXDB2_URL')) {
            $this->influxDB = new Client([
                "url" => env('INFLUXDB2_URL'),
                "token" => env('INFLUXDB2_TOKEN'),
                "bucket" => env('INFLUXDB2_BUCKET'),
                "org" => env('INFLUXDB2_ORG'),
                "precision" => WritePrecision::NS
            ]);
            $this->influxDBWriteApi = $this->influxDB->createWriteApi();
        }
        set_time_limit(0);
        $this->matcherMaven = '/(?<groupId>.+)\/(?<artifactId>.+)\/(?<version>(\d+\.?)+)(-SNAPSHOT)?\/(?P=artifactId)-(?P=version)(-(\d+.)+)?(?<classifier>-\w+)?(?<extension>\.jar|\.xml|\.pom)$/';
    }

    public function upload(Request $request, $name, $path = '')
    {
    }
    public function browse($name, $path = '')
    {
        $repository = Repository::where(['name'   =>  $name])->firstOrFail();
        if ($repository->type == 'virtual') {
            return $repository->virtuals()->get()->map(function ($repo) use ($path) {
                return $this->browse($repo->name, $path);
            })->collapse();
        } else {
            $fullPath = "repositories/$repository->name/$path";
            if (!Str::endsWith($fullPath, '/'))
                $fullPath = $fullPath . '/';
            $files = collect($this->disk->files($fullPath))->map(function ($file) use ($fullPath) {
                return [
                    'name'  =>  Str::replaceFirst($fullPath, '', $file),
                    'updated_at'    => $this->disk->lastModified($file),
                    'size'    =>  $this->disk->size($file),
                    'full'    =>  '/' . $file,
                    'type'    => 'file'
                ];
            });
            $dirs = collect($this->disk->directories($fullPath))->map(function ($dir) use ($fullPath) {
                return [
                    'name'  =>  Str::replaceFirst($fullPath, '', $dir),
                    'updated_at' => $this->disk->lastModified($dir),
                    'size'    =>  '-',
                    'type'  => 'folder'
                ];
            });
            return $dirs->concat($files);
        }
    }
    /**
     * Fetch a artifact by givin path
     * @param request
     * @param repository  The repository
     * @param path The path
     */
    public function get(Request $request, $name, $path, $localCache = false)
    {
        try {
            \Illuminate\Support\Facades\Log::info("Trying $name - $path with localCache=$localCache");
            $repository = Repository::where(['name'   =>  $name])->firstOrFail();
            if ($repository->offline) {
                return response('Crystal v1.2 - Requested repository is offline.', 404);
            }
            if ($repository->rules()->get()->every(function ($rule) use ($path) {
                return $rule->isValid($path);
            })) {
                if ($repository->type == 'local' || ($localCache && $repository->type != 'virtual')) {
                    return $this->getLocal($request, $repository, $path);
                } else if ($repository->type == 'remote') {
                    return $this->getRemote($request, $repository, $path);
                } else if ($repository->type == 'virtual') {
                    return $this->getVirtual($request, $repository, $path);
                } else if($repository->type == 'stream') {
                    return $this->getStream($request, $repository, $path);
                }
            } else {
                header('X-Crystal-Reason: Rules blocked.');
                return response('Crystal v1.2 - Requested url is blocked.', 404);
            }
        } catch (\Exception $e) {
            \Illuminate\Support\Facades\Log::error($e);
            return response('', 404);
        }
    }
    private function getStream(Request $request, Repository $repository, $path)
    {
        $url        =   $repository->upstream_url . $path;
        $repository->timingsStart();
        $headers     =   $this->getHeaders($request, $url);
        $repository->timingsStop();
        stream_context_set_default(
            [
                'http' => [
                    'method'            => 'GET',
                    'user_agent'        => $request->header('User-Agent'),
                    'protocol_version'  => 1.1,
                    'timeout'           => 10
                ],
                'ssl' => [
                    'verify_peer'       => false,
                    'verify_peer_name'  => false,
                ],
            ]
        );
        if ($headers['status'] >= 400)
            return response('Crystal v1.2 - Unable to find specified resource.', 404);
        $this->logRequest($request, $repository, $path, $headers['Content-Length'] ?? 0);
        $this->updateAssetIndex($repository, $path);
        $this->serveAttachment(NULL, $headers['Content-Type'] ?? 'application/octet-stream', $headers['Content-Length'] ?? NULL, $url);
        return true;
    }

    private function getRemote(Request $request, Repository $repository, $path)
    {
        $file = $this->getLocal($request, $repository, $path);
        if (!$file) {
            $url        =   $repository->upstream_url . $path;
            $repository->timingsStart();
            $headers     =   $this->getHeaders($request, $url);
            $repository->timingsStop();
            stream_context_set_default(
                [
                    'http' => [
                        'method'            => 'GET',
                        'user_agent'        => $request->header('User-Agent'),
                        'protocol_version'  => 1.1,
                        'timeout'           => 10
                    ],
                    'ssl' => [
                        'verify_peer'       => false,
                        'verify_peer_name'  => false,
                    ],
                ]
            );
            if ($headers['status'] >= 400)
                return response('Crystal v1.2 - Unable to find specified resource.', 404);
            $this->logRequest($request, $repository, $path, $headers['Content-Length'] ?? 0);
            $this->updateAssetIndex($repository, $path);
            $this->serveAttachment("repositories/$repository->name/$path", $headers['Content-Type'] ?? 'application/octet-stream', $headers['Content-Length'] ?? NULL, $url);
        }
        return $file;
    }

    private function getLocal(Request $request, Repository $repository, $path)
    {
        $fullPath = "repositories/$repository->name/$path";
        if ($this->disk->exists($fullPath)) {
            header('X-Crystal-Cached-At: ' . Carbon::createFromTimestamp($this->disk->lastModified($fullPath)));
            $this->logRequest($request, $repository, $path, $this->disk->size($fullPath));
            $this->updateAssetIndex($repository, $path);
            $this->serveAttachment("repositories/$repository->name/$path", NULL, $this->disk->size($fullPath));
        }
        return false;
    }

    private function getVirtual(Request $request, Repository $repository, $path)
    {
        if($repository->category == 'maven') {
            preg_match($this->matcherMaven, $path, $matches);
            if($matches && isset($matches['groupId'])) {
                AssetIndex::where(['key' => $matches['groupId'].'/'.$matches['artifactId']])->each(function ($assetIndex) use ($request, $repository, $path) {
                    if (headers_sent())
                        die();
                    $this->get($request, $assetIndex->repository->name, $path, true);
                });
            }
        }

        // Recursively searching...
        $repository->virtuals()->where('type', 'local')->each(function ($repo) use ($request, $repository, $path) {
            if (headers_sent())
                die();
            $this->get($request, $repo->name, $path, true);
        });
        $repository->virtuals()->where('type', 'remote')->orderBy('delay')->each(function ($repo) use ($request, $repository, $path) {
            if (headers_sent())
                die();
            $this->get($request, $repo->name, $path);
        });
        $repository->virtuals()->where('type', 'stream')->orderBy('delay')->each(function ($repo) use ($request, $repository, $path) {
            if (headers_sent())
                die();
            $this->get($request, $repo->name, $path);
        });
        $repository->virtuals()->where('type', 'virtual')->each(function ($repo) use ($request, $repository, $path) {
            if (headers_sent())
                die();
            $this->get($request, $repo->name, $path);
        });
        if (headers_sent())
            die();
        return response('Crystal v1.2 - Unable to find specified resource.', 404);
    }

    private function getHeaders(Request $request, $url)
    {
        stream_context_set_default(
            [
                'http' => [
                    'method'            => 'HEAD',
                    'user_agent'        => $request->header('User-Agent'),
                    'protocol_version'  => 1.1,
                    'timeout'           => 10
                ],
                'ssl' => [
                    'verify_peer'       => false,
                    'verify_peer_name'  => false,
                ],
            ]
        );
        $headers = [];
        $statusLine = '';
        try {
            $headers = get_headers($url, 1);
            $statusLine = array_reverse($headers)[0];
        } catch (\Exception $e) {
            foreach ($headers as $key => $value) {
                $length = strlen($key);
                if (substr($key, 0, $length) === 'HTTP/') {
                    $statusLine = $key;
                    break;
                }
            }
        }
        $status = explode(' ', $statusLine, 3);
        if (count($status) < 2) {
            $status = [
                '',
                '404'
            ];
        }
        $headers['status']  = intval($status[1]);
        return collect($headers)->map(function ($value, $key) {
            if (is_array($value))
                return collect($value)->last();
            else
                return $value;
        });
    }

    private function serveAttachment($fullPath, $contentType = NULL, $contentLength = NULL, $url = NULL)
    {
        if($fullPath)
            $fullPath = $this->disk->path($fullPath);
        header('Content-Description: Crystal OSS');
        header('Content-Type: ' . $contentType ?? mime_content_type($fullPath));
        if (!is_null($contentLength) && intval($contentLength) > 0)
            header('Content-Length: ' . $contentLength);
        header('Content-Disposition: attachment; filename="' . basename($fullPath) . '"');
        if(is_null($fullPath)) {
            return $this->stream_chunked($url, intval($contentLength));
        } else {
            return $this->readfile_chunked($fullPath, $url, intval($contentLength));
        }
    }

    private function stream_chunked($url, $size = NULL)
    {
        $chunksize = (1024 * 1024); // 512 Kibs

        $buffer = '';
        $handle = fopen($url, 'r');
        if ($handle === false) {
            return false;
        }
        ignore_user_abort(true); // At least the download should be completed.
        while (!feof($handle)) {
            $buffer = fread($handle, $chunksize);
            echo $buffer;
            ob_flush();
            flush();
        }
        $status = fclose($handle);
        die();
        return $status;
    }

    private function readfile_chunked($fullPath, $url = NULL, $size = NULL)
    {
        $chunksize = (1024 * 1024); // 512 Kibs

        $buffer = '';
        $cnts = 0;
        $copy   = false;
        $dirname = dirname($fullPath);
        if (!is_dir($dirname)) {
            mkdir($dirname, 0755, true);
        }
        $handle = fopen($url ?? $fullPath, 'r');
        if ($handle === false) {
            return false;
        }
        if (!is_null($url)) {
            ignore_user_abort(true); // At least the download should be completed.
            $copy = fopen($fullPath, 'wb');
            if ($copy === false) {
                $url = NULL;
            }
        }
        while (!feof($handle)) {
            $buffer = fread($handle, $chunksize);
            echo $buffer;
            ob_flush();
            if (!is_null($url)) {
                fwrite($copy, $buffer);
            }
            flush();
        }
        if (!is_null($url)) {
            fclose($copy);
        }
        $status = fclose($handle);
        die();
        return $status;
    }

    private function logRequest(Request $request, $repository, $path, $size)
    {
        if(env('INFLUXDB2_URL')) {
            $this->influxDBWriteApi->write([
                'name' => 'crystal',
                'tags' => [
                    'repo'  => $repository->name,
                ],
                'fields' => [
                    'path'  => $path,
                    'bytes' => intval($size)
                ]
            ]);
            $this->influxDBWriteApi->write([
                'name'  => 'clients',
                'tags' => [
                    'repo'  => $repository->name,
                ],
                'fields' => [

                ]
            ]);
        } else {
            // Fallback to legacy
            $log = new RequestLog;
            $log->repository_id = $repository->id;
            $log->path = $path;
            $log->ip = $request->ip();
            $log->size = $size;
            $log->save();
        }

    }
    private function updateAssetIndex(Repository $repository, $path) {
        if($repository->category == 'maven') {
            preg_match($this->matcherMaven, $path, $matches);
            if($matches && isset($matches['groupId'])) {
                $assetIndex = AssetIndex::firstOrCreate([
                    'key' => $matches['groupId'].'/'.$matches['artifactId'],
                    'repository_id' => $repository->id
                ]);
            }
        }
    }
}
